package munci.old.know;

import java.io.PrintStream;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.LinkedList;

import munci.old.use.Methods;
import munci.old.use.Tokens;
import munci.old.use.Try;


public class Stack {

	/**
	 * stack of interleaved class and object
	 */
	private final LinkedList<Object> stack = new LinkedList<Object>();

	/**
	 * Pushes a value and type onto the stack
	 * @param obj may be null
	 * @param c the class; may only be null if obj is null
	 */
	public void push(Object obj, Class<?> c) {
		c = getClass(obj, c);
		stack.addFirst(obj);
		stack.addFirst(c);
	}
	
	public void pushUnder(Object obj, Class<?> c) {
		stack.addLast(getClass(obj, c));
		stack.addLast(obj);
	}
	
	private Class<?> getClass(Object obj, Class<?> c) {
		Class<?> exact = obj.getClass();
		if (!c.isPrimitive() && !c.isAssignableFrom(exact)) {
			throw new IllegalArgumentException(c.getSimpleName() + " /= " + exact.getSimpleName());
		}
		return obj != null && c.isAssignableFrom(exact)? exact : c;
	}

	public Object pop() {
		stack.removeFirst();
		return stack.removeFirst();
	}
	
	public Object popUnder() {
		Object result = stack.removeLast();
		stack.removeLast();
		return result;
	}
	
	private Object remove(int depth) {
		stack.remove(depth * 2);
		return stack.remove(depth * 2);
	}
	
	public Object peek() {
		return stack.get(1);
	}
	
	public void dup() {
		stack.addAll(0, stack.subList(0, 2));
	}
	
	public void swap() {
		stack.add(3, stack.remove());
		stack.add(3, stack.remove());
	}
	
	public void over(int depth) {
		stack.add(1 + 2 * depth, stack.remove());
		stack.add(1 + 2 * depth, stack.remove());
	}
	
	public int depth() {
		return stack.size() / 2;
	}
	
	public Object[] values() {
		Object[] ar = new Object[depth()];
		int x = 0, i = depth() - 1;
		for (Object obj : stack) {
			if (x++ % 2 == 1) {
				ar[i--] = obj;
			}
		}
		return ar;
	}

	public Class<?> peekClass(int depth) {
		return (Class<?>) stack.get(depth * 2);
	}
	
	public void print(PrintStream stream) {
		for (int i = 0; i < stack.size(); i += 2) {
			String type = ((Class<?>) stack.get(i)).getName();
			stream.print(type.substring(type.lastIndexOf('.') + 1).replace('$', '.'));
			stream.print(":");
			stream.println(String.valueOf(stack.get(i + 1)));
		}
	}
	
	@Override public String toString() {
		return Arrays.asList(values()).toString();
	}
	
	public boolean invoke(Method method, Object obj) {
		Class<?>[] params = method.getParameterTypes();
		if (params.length > depth()) return false;
		
		for (int i = 0; i < params.length; i++) {
			if (!Methods.argumentValid(params[params.length - 1 - i], peekClass(i)))
				return false;
		}
	
		Object[] args = new Object[params.length];
		for (int i = args.length - 1; i >= 0; i--) {
			args[i] = pop();
		}
	
		Object val = Try.invoke(method, obj, args);
	
		if (method.getReturnType() != void.class) push(val, method.getReturnType());
		return true;
	}
	
	public boolean invoke(Method method, Object obj, Stack src) {
		Class<?>[] params = method.getParameterTypes();
		if (params.length != src.depth()) return false;
		
		for (int i = 0; i < params.length; i++) {
			if (!Methods.argumentValid(params[params.length - 1 - i], src.peekClass(i)))
				return false;
		}
	
		Object[] args = src.values();
		Object val = Try.invoke(method, obj, args);
	
		if (method.getReturnType() != void.class) push(val, method.getReturnType());
		return true;
	}
	
	public boolean compile(Method method, Tokens obj) {
		int arity = method.getParameterTypes().length;
		if (arity > depth()) return false;
		
		for (int i = 0; i < arity; i++) {
			if (!Tokens.class.isAssignableFrom(peekClass(i)))
				return false;
		}

		Tokens toks = new Tokens();
		if (obj != null) {
			toks.addAll(obj);
		} else {
			toks.lex(method.getDeclaringClass().getCanonicalName());
		}
		toks.add(".").add(method.getName()).add("(");

		for (int i = arity; i > 0; i--) {
			toks.addAll((Tokens) remove(i));
			if (i > 1) toks.add(",");
		}

		push(toks.add(")"), Tokens.class);
		return true;
	}
}
